# plugs/collective.py
#
# 

""" collective connections """

__copyright__ = 'this file is in the public domain'

from ommbot.world import rlog, get_datadir, add_cmnd, add_example, get_plugs
from ommbot.generic import geturl, handle_exception
from ommbot.rsslist import rsslist
from ommbot.persist import Persist
from ommbot.thr import start_new_thread
from xml.sax.saxutils import unescape
import Queue
import time
import socket
import re
import os

coll = Persist(get_datadir() + os.sep + 'collective')
plugs = get_plugs()

if not coll.data:
    coll.data = {}
if not coll.data.has_key('enable'):
    coll.data['enable'] = 0
if not coll.data.has_key('nodes'):
    coll.data['nodes'] = []
if not coll.data.has_key('names'):
    coll.data['names'] = {}
if not coll.data.has_key('active'):
    coll.data['active'] = []
if not coll.data.has_key('wait'):
    coll.data['wait'] = 5

activechecker = None

def init():
    """ init the collective plugin """
    global activechecker
    if not coll.data['enable']:
        return 1
    start_new_thread(boot, ())
    activechecker = Activechecker()
    return 1

def shutdown():
    if activechecker:
        activechecker.stop = 1

class Activechecker(object):

    """ check nodes if they are alive """

    def __init__(self):
        self.stop = 0
        start_new_thread(self.checker, ())
        rlog(10, 'collective', 'active checker started')
        
    def checker(self):
        coll.data['active'] = list(coll.data['nodes'])
        while not self.stop:
            time.sleep(900)
            if self.stop:
                break
            self.checkone()
        rlog(10, 'collective', 'stopping activechecker')

    def checkone(self):
        removed = []
        added = []
        for i in coll.data['nodes']:
            if not doping(i):
                try:
                    if not i in coll.data['active']:
                        continue
                    coll.data['active'].remove(i)
                    removed.append(i)
                    rlog(10, 'collective', 'removed %s from active list' % i)
                except ValueError:
                    pass
            else:
                if not i in coll.data['active']:
                    coll.data['active'].append(i)
                    added.append(i)
                    rlog(10, 'collective', '%s added to active list' % i)
        return (removed, added)

def doping(node):
    q = Queue.Queue()
    t = start_new_thread(gotpong, (node, q))
    result = []
    while 1:
        try:
            res = q.get(1, 10)
        except (Queue.Empty, socket.timeout):
            break
        if not res:
            break
        result.append(res)
    return result
         
def gotpong(node, queue):
    """ check if node returns pong """
    try:
        pongtxt = geturl("http://%s/ping&how=direct" % node)
    except Exception, ex:
        pongtxt = ""
    if 'pong' in pongtxt:
        if queue:
            queue.put_nowait(1)
            queue.put_nowait(None)
        else:
            return 1
    else:
        if queue:
            queue.put_nowait(None)

def getnodes():
    """ return cached nodes cache """
    return coll.data['nodes']

def getactive():
    """ return active nodes """
    return coll.data['active']

def getname(node):
    """ return name of node or None """
    if coll.data['names'].has_key(node):
        return coll.data['names'][node]

def getnodefromname(name):
    for i, j in coll.data['names'].iteritems():
        if j == name:
            return i

def addnode(node):
    """ add a node to cache """
    (host, port) = node.split(':')
    try:
        ipnr = socket.gethostbyname(host)
        if ipnr:
            node = "%s:%s" % (ipnr, port)
    except:
        pass
    if node not in coll.data['nodes']:
        rlog(0, 'collective', 'adding node %s (%s)' % (str(node), \
getname(node)))
        coll.data['nodes'].append(node)
        if doping(node):
            coll.data['active'].append(node)
        coll.save()
        return 1
    return 0

def syncnode(node):
    """ sync cacne with node """
    try:
        result = geturl('http://%s/nodes&how=noescape' % node)
    except:
        rlog(10, 'collective', "can't fetch %s data" % node)
        return 0
    rss = rsslist(result)
    got = 0
    for i in rss:
        try:
            node = i['node']
            if addnode(node):
                got = 1
        except:
            handle_exception()
            continue
        try:
            if not coll.data['names'].has_key(node):
                coll.data['names'][node] = i['name']
        except:
            pass
    if got:
        coll.save()
    return 1

def colldispatchone(node, what, queue):
    """ dispatch command on remote node .. place result in queue """
    colldispatch(node, what, queue)
    queue.put(None)

def colldispatch(node, what, queue):
    """ dispatch command on remote node .. place result in queue """
    try:
        what = re.sub('\s+', '+', what)
        result = geturl('http://%s/dispatch?%s&how=direct' % (node, what))
    except:
        rlog(10, 'collective', "can't fetch %s data" % node)
        return 0
    name = getname(node)
    if not name:
        name = node
    res = result.split('\n\n')
    if len(res) > 1:
        for i in res:
            if not i:
                continue
            queue.put((name, i))
    else:
        queue.put((name, res[0]))

def colldispatchall(what):
    """ coll dispatch on all nodes """
    queue = Queue.Queue()
    for i in coll.data['active']:
        start_new_thread(colldispatch, (i, what, queue))
    for i in range(coll.data['wait']*10):
        if queue.qsize() == len(coll.data['active']):
            break
        time.sleep(0.1)
    queue.put(None)
    return queue
        
def boot(server=None):
    """ sync cache with main server or server provided """
    if not server:
        syncnode('r8.cg.nu:8088')
    else:
        syncnode(server)
    nrnodes = len(coll.data['nodes'])
    rlog(10, 'collective', 'booted %s nodes' % nrnodes)
    return nrnodes
    
def nodesxml():
    """ return nodes in xml format """
    result = "<xml>\n"
    gotit = False
    for i in coll.data['nodes']:
        gotit = True
        result += "    <coll>\n"
        result += "        <node>%s</node>\n" % i
        name = getname(i)
        if name: 
            result += "        <name>%s</name>\n" % name
        result += "    </coll>\n"
    if gotit:
        result += "</xml>"
        return result
    return ""
        
def handle_colllist(bot, ievent):
    """ coll-list .. list all nodes in cache """
    if not coll.data['enable']:
        ievent.reply('collective is not enabled')
        return
    ievent.reply(str(coll.data['nodes']))

add_cmnd('coll-list', handle_colllist, 'OPER')
add_example('coll-list', 'list nodes cache', 'coll-list')

def handle_collenable(bot, ievent):
    """ coll-enable .. enable the collective """
    coll.data['enable'] = 1
    coll.save()
    plugs.reload('plugs', 'collective')
    ievent.reply('collective enabled')

add_cmnd('coll-enable', handle_collenable, 'OPER')
add_example('coll-enable', 'enable the collective', 'coll-enable')

def handle_colldisable(bot, ievent):
    """ coll-disable .. disable the collective """
    coll.data['enable'] = 0
    coll.save()
    plugs.reload('plugs', 'collective')
    ievent.reply('collective disabled')

add_cmnd('coll-disable', handle_colldisable, 'OPER')
add_example('coll-disable', 'disable the collective', 'coll-disable')

def handle_collsync(bot, ievent):
    """ coll-sync <node> .. sync nodes cache with node """ 
    if not coll.data['enable']:
        ievent.reply('collective is not enabled')
        return
    try:
        node = ievent.args[0]
    except IndexError:
        ievent.missing('<node>')
        return
    result = syncnode(node)
    ievent.reply('%s nodes added' % result)

add_cmnd('coll-sync', handle_collsync, 'OPER')
add_example('coll-sync', 'coll-sync <node> .. sync with provided node', \
            'coll-sync r8.cg.nu')

def handle_coll(bot, ievent):
    """ coll <cmnd> .. execute <cmnd> on nodes """
    if not coll.data['enable']:
        ievent.reply('collective is not enabled')
        return
    if not ievent.rest:
        ievent.missing('<command>')
        return
    starttime = time.time()
    result = colldispatchall(ievent.rest)
    resultstr = ""
    total = len(coll.data['nodes'])
    teller = 0
    got = []
    while 1:
        try:
            res = result.get(1, coll.data['wait'])
        except Queue.Empty :
            break
        if not res:
            break
        resultstr += "-||%s||- %s " % (res[0], unescape(res[1]))
        if res[0] not in got:
            teller += 1
            got.append(res[0])
    if resultstr:
        ievent.reply('%s out of %s (%s) ==> %s' % (teller, total, \
time.time()-starttime, resultstr))
    else:
        ievent.reply('no results found')

add_cmnd('coll', handle_coll, ['USER', 'WEB'])
add_example('coll', 'coll <cmnd> .. execute command in the collective', \
            'coll lq')

def handle_collexec(bot, ievent):
    """ coll <nodename> <cmnd> .. execute <cmnd> on node """
    if not coll.data['enable']:
        ievent.reply('collective is not enabled')
        return
    try:
        (name, command) = ievent.rest.split(' ', 1)
    except ValueError:
        ievent.missing('<nodename> <command>')
        return
    node = getnodefromname(name)
    if not node:
        ievent.reply('no node %s found' % name)
        return
    starttime = time.time()
    queue = Queue.Queue()
    start_new_thread(colldispatchone, (node, command, queue))
    resultstr = ""
    while 1:
        try:
            res = queue.get(1, coll.data['wait'])
        except Queue.Empty:
            break
        if not res:
            break
        resultstr += "-||%s||- %s " % (name, unescape(res[1]))
    if resultstr:
        ievent.reply("(%s) %s" % (time.time()-starttime, resultstr))
    else:
        ievent.reply('no results found')

add_cmnd('coll-exec', handle_collexec, ['USER', 'WEB'])
add_example('coll-exec', 'coll <nodename> <cmnd> .. execute command in \
             the collective', 'coll r8 lq')

def handle_colladdnode(bot, ievent):
    """ coll-addnode <name> <host:port> .. add node to cache """
    try:
        (name, node) = ievent.args
    except ValueError:
        ievent.missing('<name> <host:port>')
        return
    if ':' not in node:
        ievent.missing('<name> <host:port>')
        return
    (host, port) = node.split(':')
    try:
        ipnr = socket.gethostbyname(host)
        if ipnr:
            node = "%s:%s" % (ipnr, port)
    except:
        ievent.reply("can't find ipnr for %s" % host)
        return
    coll.data['names'][node] = name
    addnode(node)
    coll.save()
    ievent.reply('%s added' % name)

add_cmnd('coll-addnode', handle_colladdnode, 'OPER')
add_example('coll-addnode', 'coll-addnode <name> <node> .. add a node to \
             cache .. node is in host:port format', 'coll-addnode r8 \
             r8.cg.nu:8088')

def handle_colldelnode(bot, ievent):
    """ coll-delnode <name> .. delete node from cache """
    try:
        what = ievent.args[0]
    except IndexError:
        ievent.missing('<name>')
        return
    try:
        for i, j in coll.data['names'].iteritems():
            if j == what:
                coll.data['nodes'].remove(i)
                if i in coll.data['active']:
                    coll.data['active'].remove(i)
                coll.save()
                ievent.reply('%s deleted' % what)
                return
        ievent.reply("can't find node %s" % what)
    except ValueError:
        ievent.reply('failed to remove %s' % what)

add_cmnd('coll-delnode', handle_colldelnode, 'OPER')
add_example('coll-delnode', 'coll-delnode <name> .. remove node from \
             collective list', 'coll-delnode r8')

def handle_collsetname(bot, ievent):
    """ coll-setname <node> <name> .. set name of node """
    try:
        (what, name) = ievent.args
    except ValueError:
        ievent.missing('<node> <name>')
        return
    coll.data['names'][what] = name
    coll.save()
    ievent.reply('%s added' % name)
 
add_cmnd('coll-setname', handle_collsetname, 'OPER')
add_example('coll-setname', 'set name of collective node', 'coll-setname \
             r8 r8.cg.nu:8088')

def handle_collnames(bot, ievent):
    """ coll-names .. show names with nodes in cache """
    ievent.reply(str(coll.data['names'].values()))
 
add_cmnd('coll-names', handle_collnames, 'OPER')
add_example('coll-names', 'show collective nodes with there names', \
            'coll-names')

def handle_collboot(bot, ievent):
    """ boot the collective node cache """
    try:
        server = ievent.args[0]
    except:
        server = 'r8.cg.nu:8088'
    bootnr = boot(server)
    ievent.reply('collective at %s nodes' % bootnr)
 
add_cmnd('coll-boot', handle_collboot, 'OPER')
add_example('coll-boot', 'sync collective list with provided host', \
            'coll-boot')

def handle_collfullboot(bot, ievent):
    """ coll-fullboot .. boot from all nodes in cache """
    try:
        (server, port) = ievent.args
    except ValueError:
        server = 'r8.cg.nu'
        port = 8088
    teller = 0
    snap = list(coll.data['nodes'])
    for i in snap:
        boot(i)
        teller += 1
    ievent.reply('%s nodes checked .. current %s nodes in list' % (teller, 
len(coll.data['nodes'])))
 
add_cmnd('coll-fullboot', handle_collfullboot, 'OPER')
add_example('coll-fullboot', 'do a boot on every node in the collective \
             list', 'coll-fullboot')

def handle_collsave(bot, ievent):
    """ coll-save .. save collective data to disk """
    coll.save()
    ievent.reply('collective saved')
 
add_cmnd('coll-save', handle_collsave, 'OPER')
add_example('coll-save', 'save collective data' , 'coll-save')

def handle_clean(bot , ievent):
    """ coll-clean .. clear nodes cache """
    coll.data['nodes'] = []
    coll.data['active'] = []
    coll.save()
    ievent.reply('done')

add_cmnd('coll-clean', handle_clean, 'OPER')
add_example('coll-clean', 'clean the collective list', 'coll-clean')

def handle_collactive(bot, ievent):
    """ show active nodes """
    resultstr = ""
    for i in coll.data['active']:
        name = getname(i)
        resultstr += "%s (%s) .. " % (i, name)
    if resultstr:
        ievent.reply(resultstr[:-4])
    else:
        ievent.reply("no nodes active")
        
add_cmnd('coll-active', handle_collactive, 'OPER')
add_example('coll-active', 'show active nodes', 'coll-active')

def handle_collinactive(bot, ievent):
    """ show inactive nodes """
    resultstr = ""
    for i in coll.data['nodes']:
        if i not in coll.data['active']:
            name = getname(i)
            resultstr += "%s (%s) .. " % (i, name)
    if resultstr:
        ievent.reply(resultstr[:-4])
    else:
        ievent.reply("no nodes inactive")
        
add_cmnd('coll-inactive', handle_collinactive, 'OPER')
add_example('coll-inactive', 'show inactive nodes', 'coll-inactive')

def handle_collcheckactive(bot, ievent):
    """ run active check """
    result = activechecker.checkone()
    ievent.reply('%s removed %s added' % result)

add_cmnd('coll-checkactive', handle_collcheckactive, 'OPER')
add_example('coll-checkactive', 'run active nodes check', 'coll-checkactive')
